/**
 * 
 */
package org.statewalker.fsm.providers;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class FsmTransitionsRegistry implements IFsmTransitionsRegistry {

    private static class LeafStateTransitions extends Transitions {

        public LeafStateTransitions() {
            super();
        }

        @Override
        public void addTargetStateKeys(Set<String> set) {
        }

        @Override
        public boolean equals(Object obj) {
            return super.equals(obj);
        }

        @Override
        public String getDefaultTransition(String event) {
            return FsmBase.FINAL;
        }

        @Override
        public String getTransition(String from, String event) {
            return FsmBase.FINAL;
        }

        @Override
        public int hashCode() {
            return super.hashCode();
        }

        @Override
        public String toString() {
            return "[LeafTransitions]";
        }
    }

    private static class NormalStateTransitions extends Transitions {

        private Map<String, String> fDefaultTransitions = new HashMap<String, String>();

        private Map<String, Map<String, String>> fTransitions = new HashMap<String, Map<String, String>>();

        public NormalStateTransitions() {
            super();
        }

        private void addAllStateKeys(
            Set<String> set,
            Map<String, String> transitions) {
            for (String stateKey : transitions.values()) {
                addStateKey(set, stateKey);
            }
        }

        public void addDefaultTransition(String event, String to) {
            if (to == null) {
                to = FsmBase.FINAL;
            }
            if (event == null) {
                event = FsmBase.ANY_EVENT_KEY;
            }
            fDefaultTransitions.put(event, to);
        }

        private void addStateKey(Set<String> set, String stateKey) {
            if (FsmBase.ANY_EVENT_KEY.equals(stateKey)
                || FsmBase.FINAL.equals(stateKey)
                || FsmBase.INITIAL.equals(stateKey)) {
                return;
            }
            set.add(stateKey);

        }

        @Override
        public void addTargetStateKeys(Set<String> set) {
            addAllStateKeys(set, fDefaultTransitions);
            for (Map.Entry<String, Map<String, String>> entry : fTransitions
                .entrySet()) {
                String stateKey = entry.getKey();
                addStateKey(set, stateKey);
                Map<String, String> transitions = entry.getValue();
                addAllStateKeys(set, transitions);
            }
        }

        public void addTransition(String from, String event, String to) {
            if (from == null) {
                from = FsmBase.INITIAL;
            }
            if (to == null) {
                to = FsmBase.FINAL;
            }
            if (event == null) {
                event = FsmBase.ANY_EVENT_KEY;
            }
            Map<String, String> transitionMap = getTransitionMap(from, true);
            transitionMap.put(event, to);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof NormalStateTransitions)) {
                return false;
            }
            NormalStateTransitions o = (NormalStateTransitions) obj;
            return fTransitions.equals(o.fTransitions)
                && fDefaultTransitions.equals(o.fDefaultTransitions);
        }

        @Override
        public String getDefaultTransition(String event) {
            if (event == null) {
                event = FsmBase.ANY_EVENT_KEY;
            }
            String result = fDefaultTransitions.get(event);
            return result;
        }

        @Override
        public String getTransition(String from, String event) {
            if (from == null) {
                from = FsmBase.INITIAL;
            }
            if (event == null) {
                event = FsmBase.ANY_EVENT_KEY;
            }
            Map<String, String> transitionMap = getTransitionMap(from, false);
            String result = transitionMap != null
                ? transitionMap.get(event)
                : null;
            return result;
        }

        private Map<String, String> getTransitionMap(String from, boolean create) {
            Map<String, String> transitionMap = fTransitions.get(from);
            if (transitionMap == null && create) {
                transitionMap = new HashMap<String, String>();
                fTransitions.put(from, transitionMap);
            }
            return transitionMap;
        }

        @Override
        public int hashCode() {
            return fTransitions.hashCode() ^ fDefaultTransitions.hashCode();
        }

        @Override
        public String toString() {
            return "[Transitions="
                + fTransitions
                + ";DefaultTransitions="
                + fDefaultTransitions
                + "]";
        }
    }

    /**
     * Instances of this type are used to keep transitions between substates.
     * 
     * @author kotelnikov
     */
    private abstract static class Transitions {

        public Transitions() {
        }

        /**
         * This method adds all target state keys to the given set
         * 
         * @param set the set where all target keys should be added
         */
        public abstract void addTargetStateKeys(Set<String> set);

        /**
         * @param event the event activating the transition
         * @return the default transition for the specified event
         */
        public abstract String getDefaultTransition(String event);

        /**
         * Returns the target state of a transition from the specified state
         * with the given event
         * 
         * @param from
         * @param event
         * @return
         */
        public abstract String getTransition(String from, String event);

    }

    private Map<String, String> fParents = new HashMap<String, String>();

    /**
     * This map contains properties associated with individual states.
     */
    private Map<String, Map<String, String>> fProperties = new HashMap<String, Map<String, String>>();

    private final Map<String, Transitions> fTransitions = new HashMap<String, Transitions>();

    public FsmTransitionsRegistry() {
    }

    /**
     * @param parent
     * @param event
     * @param to
     */
    public void addDefaultTransition(String parent, String event, String to) {
        NormalStateTransitions transitions = getSimpleTransitions(parent, true);
        transitions.addDefaultTransition(event, to);
    }

    /**
     * Adds a new property value associated with the specified state
     * 
     * @param state the key of the state
     * @param name the name of the property
     * @param value the value of the property
     */
    public void addStateProperty(String state, String name, String value) {
        Map<String, String> properties = getPropertyMap(state, true);
        properties.put(name, value);
    }

    /**
     * @param parent
     * @param from
     * @param event
     * @param to
     */
    public void addTransition(
        String parent,
        String from,
        String event,
        String to) {
        NormalStateTransitions transitions = getSimpleTransitions(parent, true);
        transitions.addTransition(from, event, to);
    }

    /**
     * Defines sequential transitions between states. This method do the same as
     * the {@link #addTransitionsForEvent(String, String, String...)} method
     * with the <code>event</code> parameter equals to
     * {@link FsmBase#ANY_EVENT_KEY}.
     * 
     * @param parent the parent state
     * @param sequence the sequence of states
     */
    public void addTransitions(String parent, String... sequence) {
        addTransitionsForEvent(parent, FsmBase.ANY_EVENT_KEY, sequence);
    }

    /**
     * Defines sequential transitions between states when the specified event is
     * occurred.
     * 
     * @param parent the parent state
     * @param event the event key
     * @param sequence the sequence of states
     */
    public void addTransitionsForEvent(
        String parent,
        String event,
        String... sequence) {
        if (sequence == null || sequence.length == 0) {
            return;
        }
        NormalStateTransitions transitions = getSimpleTransitions(parent, true);
        for (int i = 0; i < sequence.length;) {
            String prev = sequence[i++];
            String next = i < sequence.length ? sequence[i] : null;
            transitions.addTransition(prev, event, next);
        }
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof FsmTransitionsRegistry)) {
            return false;
        }
        FsmTransitionsRegistry o = (FsmTransitionsRegistry) obj;
        return fTransitions.equals(o.fTransitions);
    }

    /**
     * @see org.statewalker.fsm.providers.IFsmTransitionsRegistry#getDefaultTransition(java.lang.String,
     *      java.lang.String)
     */
    public String getDefaultTransition(String parent, String event) {
        Transitions transitions = fTransitions.get(parent);
        if (transitions == null) {
            return null;
        }
        String result = transitions.getDefaultTransition(event);
        return result;
    }

    /**
     * @see org.statewalker.fsm.providers.IFsmTransitionsRegistry#getParentEventKey(java.lang.String)
     */
    public String getParentEventKey(String event) {
        if (event == null) {
            return null;
        }
        String parentKey = fParents.get(event);
        return parentKey;
    }

    /**
     * Returns the property map corresponding to the state with the given key.
     * 
     * @param state the key of the state
     * @return the property map corresponding to the state with the given key
     */
    public Map<String, String> getProperties(String state) {
        Map<String, String> map = getPropertyMap(state, false);
        return map;
    }

    /**
     * Returns the value of the state property with the specified name.
     * 
     * @param state the name of the state
     * @param name the name of the property associated with the property
     * @return the value of the state property
     */
    public String getProperty(String state, String name) {
        Map<String, String> map = getProperties(state);
        return map != null ? map.get(name) : null;
    }

    /**
     * Returns the property map associated with the specified state.
     * 
     * @param state the key of the state
     * @param create if this flag is <code>true</code> and there is no property
     *        map for the specified state then a new map is created
     * @return the property map associated with the specified state
     */
    private Map<String, String> getPropertyMap(String state, boolean create) {
        Map<String, String> map = fProperties.get(state);
        if (map == null && create) {
            map = new LinkedHashMap<String, String>();
            fProperties.put(state, map);
        }
        return map;
    }

    /**
     * Returns a {@link NormalStateTransitions} instance for the specified
     * state. If the there is no {@link NormalStateTransitions} instance
     * associated with the state and the <code>create</code> parameter is
     * <code>true</code> then a new instance will be created and returned.
     * 
     * @param state the state
     * @param create if this flag is <code>true</code> and the internal map does
     *        not contain an {@link NormalStateTransitions} instance then this
     *        method creates new such an instance and returns it
     * @return a {@link NormalStateTransitions} instance for the specified state
     */
    private NormalStateTransitions getSimpleTransitions(
        String state,
        boolean create) {
        NormalStateTransitions result = null;
        Transitions transitions = fTransitions.get(state);
        if (transitions instanceof NormalStateTransitions) {
            result = (NormalStateTransitions) transitions;
        } else if (create) {
            transitions = new NormalStateTransitions();
            fTransitions.put(state, transitions);
            result = (NormalStateTransitions) transitions;
        }
        return result;
    }

    /**
     * @return a set of all state keys registered
     */
    public Set<String> getStateKeys() {
        Set<String> set = new HashSet<String>();
        for (Map.Entry<String, Transitions> entry : fTransitions.entrySet()) {
            String stateKey = entry.getKey();
            Transitions transitions = entry.getValue();
            set.add(stateKey);
            transitions.addTargetStateKeys(set);
        }
        return set;
    }

    /**
     * @see org.statewalker.fsm.providers.IFsmTransitionsRegistry#getTransition(java.lang.String,
     *      java.lang.String, java.lang.String)
     */
    public String getTransition(String parent, String from, String event) {
        Transitions transitions = fTransitions.get(parent);
        if (transitions == null) {
            return null;
        }
        String result = transitions.getTransition(from, event);
        return result;
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return fTransitions.hashCode();
    }

    /**
     * Marks the state with the given key as a "leaf" state - leaf states does
     * not have sub-states.
     * 
     * @param state the state to mark as a leaf
     */
    public void setLeafState(String state) {
        Transitions transitions = fTransitions.get(state);
        if (!(transitions instanceof LeafStateTransitions)) {
            transitions = new LeafStateTransitions();
            fTransitions.put(state, transitions);
        }
    }

    /**
     * @param token
     * @param parent
     */
    public void setParentToken(String token, String parent) {
        fParents.put(token, parent);
    }

    @Override
    public String toString() {
        return fTransitions.toString();
    }
}